fix(deploy): handle SAS9 execution error gracefully - #1065
fix(deploy): handle SAS9 execution error gracefully#1065krishna-acondy wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Hermes Agent Code Review
Verdict: Request changes — the core feature is effectively unreachable on the SAS9 path due to branch ordering; merge conflicts must also be resolved before merge.
ℹ️ Pre-review gate: GitHub reports this PR as
mergeable: false/mergeable_state: dirty(the branch is far behindmain). No check runs have run on the head SHA. I proceeded with a code review since the change is small and self-contained, but merge conflicts should be resolved before merging.
Summary
The PR adds a JobExecutionError branch to the SAS9 deploy catch handler so that, when a stored-process execution fails with errors, the SAS log is written to the usual .log file instead of being dropped. The intent (issue #1063) is sound and the approach (import JobExecutionError from the adapter, write err.result to the log path, then re-throw) is the right shape. However, the branch ordering prevents the new branch from being reached in the exact scenario the PR targets.
Correctness
- 🔴 Branch ordering defeats the feature (primary issue). The adapter's
parseError(@sasjs/adapterRequestClient.ts) constructsJobExecutionErrorwitherrorCode: 404for both the "stored process not found" case and the "Stored Process Error / This request completed with errors" case (the latter carries the valuable SAS log inresult). Because the catch handler checkserr.errorCode === 404first, everyJobExecutionErrorproduced by the SAS9 execution path — including the stored-process-error case this PR is meant to handle — is routed todisplaySasjsRunnerError(username)and never reaches theelse if (err instanceof JobExecutionError)log-saving branch. In other words, the new log-saving code is effectively dead for the SAS9 path.- Fix: check
err instanceof JobExecutionErrorbefore the generic 404 check, or distinguish the "runner missing" 404 (emptyresult) from the "stored process error" 404 (non-emptyresult). For example:.catch(async (err) => { if (err instanceof JobExecutionError) { if (err.result) { // stored process completed with errors — save the log process.logger?.error('Deployment completed with errors.') const errorLogPath = path.join( logFilePath || process.cwd(), `${path.basename(deployScript).replace('.sas', '')}.log` ) await createFile(errorLogPath, err.result) process.logger?.info(`Error log is available at ${errorLogPath}`) throw new Error('Deployment completed with errors.') } else { // runner not found displaySasjsRunnerError(username) } } else { process.logger?.error(formatErrorString(err)) } })
- Fix: check
- 🟡
throw new Error()has no message. The empty-messageErrormakes the propagated failure opaque to callers and logs. Considerthrow new Error('Deployment completed with errors. See log for details.')so the failure reason is preserved up the stack. - 🟡 Regression in the 404 branch: error detail no longer logged. Previously
formatErrorString(err)was logged for all errors before the 404 check; now the 404 branch only callsdisplaySasjsRunnerErrorand logs nothing about the underlying error. If the 404 is not the "runner missing" case (e.g. aJobExecutionErrorwitherrorCode: 404and a real message), the diagnostic detail is lost. This is mitigated if the branch ordering is fixed as above.
Consistency
- 🟡 Diverges from the sibling
runcommand.src/commands/run/run.tshandles the same SAS9 execution error by checkingerr.payload?.log(a different error shape) and throws anErrorResponse. This PR useserr instanceof JobExecutionError/err.result. The two code paths now detect SAS9 execution failures differently. Worth confirming which error shapeexecuteScriptSAS9actually rejects with in current adapter versions (the packed@sasjs/adapter@3.0.0types showJobExecutionErrorwithresult: string, andparseErroris the throw path for SAS9 responses), and aligning both commands so future maintenance doesn't diverge further.
Tests
- 🟡 No test coverage for the new branch. There are no deploy specs exercising the
JobExecutionErrorpath (thedeploy/spec/*.spec.tsfiles cover command wiring /getDeployScripts/cbd, not the SAS9 error handler). Since this is exactly the kind of branch-ordering bug that a unit test would catch ("given aJobExecutionErrorwitherrorCode: 404and a non-emptyresult, the log file is written"), a regression test is strongly recommended. Mocksasjs.executeScriptSAS9to reject with aJobExecutionError(404, 'This request completed with errors.', '<log>')and assert the.logfile is created.
Style / Minor
- 🟢
executionResult ?? ''on line 278 is a good defensive touch for the success-path log write. - The
else { process.logger?.error('Unable to create log file.') }on line 291 is now reached whenlogFilePathis falsy or when execution failed but didn't throw (e.g. the 404 branch). The message is slightly misleading for the latter; acceptable but could be more specific.
Merge readiness
- Resolve merge conflicts against
main(branch is significantly behind). - Add a unit test for the
JobExecutionErrorlog-saving path (this would have caught the ordering bug). - Fix the branch ordering so the feature actually takes effect on SAS9.
Reviewed by Hermes Agent (GitHub App)
| .catch((err) => { | ||
| process.logger?.log(formatErrorString(err)) | ||
| .catch(async (err) => { | ||
| if (err && err.errorCode === 404) { |
There was a problem hiding this comment.
🔴 Branch ordering blocks the new feature. The adapter's parseError produces a JobExecutionError with errorCode: 404 for both "stored process not found" and "Stored Process Error / This request completed with errors" (the latter carries the SAS log in result). Since this errorCode === 404 check comes first, every JobExecutionError from the SAS9 path falls into this branch and calls displaySasjsRunnerError — the else if (err instanceof JobExecutionError) branch below is never reached for SAS9. Recommend checking err instanceof JobExecutionError first, then distinguishing runner-missing (result empty) from stored-process-error (result has the log).
| ) | ||
| await createFile(errorLogPath, err.result) | ||
| process.logger?.info(`Error log is available at ${errorLogPath}`) | ||
| throw new Error() |
There was a problem hiding this comment.
🟡 throw new Error() with no message makes the propagated failure opaque to callers and to logs. Consider throw new Error('Deployment completed with errors. See log for details.').
| .catch(async (err) => { | ||
| if (err && err.errorCode === 404) { | ||
| displaySasjsRunnerError(username) | ||
| } else if (err instanceof JobExecutionError) { |
There was a problem hiding this comment.
💡 This branch is only reachable for a JobExecutionError whose errorCode is not 404. Per the adapter's parseError, all SAS9 JobExecutionErrors use errorCode: 404, so this log-saving code is effectively dead on the SAS9 path today. A unit test that mocks executeScriptSAS9 to reject with new JobExecutionError(404, 'This request completed with errors.', '<log>') and asserts the .log file is created would catch this.
| .catch((err) => { | ||
| process.logger?.log(formatErrorString(err)) | ||
| .catch(async (err) => { | ||
| if (err && err.errorCode === 404) { |
There was a problem hiding this comment.
🟡 Regression: the previous code logged formatErrorString(err) for all errors before the 404 check. Now the 404 branch logs nothing about the underlying error (only displaySasjsRunnerError). If this 404 is a JobExecutionError with a real message (not the runner-missing case), the diagnostic detail is lost.
Issue
#1063
Intent
Handle job execution errors during SAS9 deployments.
Implementation
Catch
JobExecutionErrors and save the log output to the usual log file path.sasjs/adapter#601 also fixes the adapter logic to return the correct error code so the 'missing SASjs runner' message is avoided.
A deploy that fails with stored process errors will now output this:

Checks
npm run lint:fix).npm test).